
/*
微信小程序 computed
  支持 Page Component 构造器
  弱支持 计算属性相互引用 ( 保证按顺序引用 )
  考虑小程序性能原因  computed 里的属性请在初始化时定义好 暂不开放动态添加computed属性
  参考vue  computed里的属性 this 指定当前 page | component 实例
  特别注意
    由于小程序特性  每次setData 都会重新计算computed 所有配置函数
    computed 的配置属性 最终会合并到 data 里面
    所以可以 直接修改 computed prop -> 修改 data 映射
  使用示例:
  page({
    data:{
      children: 23
    }
    ,computed:{
      parent(){
        return this.data.children + 25;
      }
    }
  })
  2021/4/24
  新增  组件  计算功能  在 mixins里 已经全局开启
  主意：
    ### 由于小程序组件构造器的方法
   1. 在定义 properties ,data 里 key不应出现  __computedOptionInData__  这个字段
   2. 必须在  app.js 开启  全局混入
  示例:
  Component({
    data:{
      age: 12
    }
    ,computed:{
      ageAdd(){
        return this.data.age + 1;
      }
    }
  })
  2022/8/31
    优化computed 在 Component 构造器上性能
    更改 computed this 指向为当前page 或者 component 实例
*/
const comUtil = require('@/utils/common')
const computedConfig = require('./config')

module.exports = function $computed(isPage = true) {
  // 是否开启 计算属性
  if (!computedConfig.openComputedAuto) {
    return
  }
  // 如果已经设置计算属性 直接阻断
  if (this[computedConfig.computedIsConfigableKey]) {
    return
  }
  const page = this
  const { data } = page
  let computed
  if (!isPage) {
    const _computed = page.data[ computedConfig.computedKey_component ]
    // computedData 转换到  实例本身
    page[ computedConfig.computedKey_component ] = _computed
    computed = _computed
    // 移除在 component.data 上的 computedData
    delete page.data[ computedConfig.computedKey_component ]
  } else {
    computed = page[computedConfig.computedKey_page]
  }

  // 设置一个变量保存computed是否设置成功 优化初始化速度
  let setDataAuto = false
  // 这些判断只在第一次初始化的时候进行判断
  // 获取computed对象类型 如果不是对象Object类型 下面不进行冲突判断
  if (computed) {
    const computedClass = comUtil.toType(computed)
    if (computedClass === 'Object') {
      // 对 computed 属性进行类型检查  如果不是函数就抛出一个错误
      // 对 computed 与 data 属性进行对比,如果有相同的就抛出一个错误
      const computedKeys = Object.keys(computed) // 利用闭包优化后续代理查询速度
      for (let i = 0, len = computedKeys.length; i < len; i++) {
        const key = computedKeys[i]
        if (typeof computed[key] !== 'function') {
          throw new TypeError(
            `the computed [${key}] property is not a function`
          )
        }
        if (comUtil.hasOwn(data, key)) {
          throw Error(`the computed [${key}] property is defined in data`)
        }
      }
      // 把setDataAuto设置为true
      setDataAuto = true
      // 保存page的原生setData函数 利用闭包优化代理setData速度
      const setDataOfNative = page.setData
      // 设置 page setData代理函数
      Object.defineProperty(page, 'setData', {
        // 不可再次配置
        configurable: false,
        enumerable: true,
        writable: false,
        value: function(dataProp) {
          // 判断data存在
          if (dataProp) {
            // 判断data类型
            if (comUtil.toType(dataProp) === 'Object') {
              const dataKeys = Object.keys(dataProp)
              // 判断传入参数是不是一个空对象(没有属性) {}  并发出一个警告
              // 声明一个变量合并改变的属性和值(包括 传入参数 和 computed属性 )
              const assignData = {}
              // 合并传参到data对象
              for (let i = 0, len = dataKeys.length; i < len; i++) {
                // 获取 传入属性
                const key = dataKeys[i]
                // 判断 computed 属性是否 与 传入属性冲突
                if (!comUtil.hasOwn(computed, key)) {
                  data[key] = assignData[key] = dataProp[key]
                } else {
                  throw Error(
                    `this attribute [${key}] has been defined in computed`
                  )
                }
              }
              //  属性合并完成后  进行一次 computed 重计算合并到 data 对象里面
              // 由于小程序setData限制 这里就不做 data 与 computed 属性冲突判断
              computedKeys.forEach((key) => {
                const setVal = computed[key].call(page)
                // 优化 对比计算后的值与 data对象的值
                if (setVal !== data[key]) {
                  data[key] = assignData[key] = setVal
                }
              })
              // 合并完成 调用原型setData渲染视图
              setDataOfNative.call(page, assignData)
            } else {
              throw new TypeError('the data is must be Object')
            }
          } else {
            throw new ReferenceError('the data in not defined')
          }
        }
      })
      // 如果外部属性写入导致冲突
      if (!comUtil.hasOwn(page, computedConfig.computedIsConfigableKey)) {
        // 设置 page 一个变量表示 setData 已经被配置过  不可配置 不可写入
        Object.defineProperty(page, computedConfig.computedIsConfigableKey, {
          value: true,
          configurable: false,
          writable: false
        })
      } else {
        throw Error(
          `open Computed , Page or Component Instance key ${computedConfig.computedIsConfigableKey}  are not configurable`
        )
      }
    } else {
      console.warn(
        `ignore: the computed ClassType is [Object ${computedClass}]`
      )
    }
  }
  /* else {
      console.warn("ignore: the computed is not defined");
    } */
  // 如果computed设置成功  则进行一次初始化  实例化computed属性到 data
  if (setDataAuto) {
    page.setData({})
  }
}
